package com.hiss.basic.starter.log.aspect;

import cn.hutool.core.date.StopWatch;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.fastjson.JSONObject;
import com.hiss.basic.starter.core.utils.ServletUtils;
import com.hiss.basic.starter.log.annotation.Log;
import com.hiss.basic.starter.log.enums.OperateStatusEnum;
import com.hiss.basic.starter.log.event.OperateLogEvent;
import com.hiss.basic.starter.security.helper.SecurityHelper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Aspect
@Configuration
public class LogAspect {

    private static final ThreadLocal<StopWatch> STOP_WATCH_THREAD_LOCAL = new ThreadLocal<>();

    private static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};

    /**
     * 方法执行前
     */
    @Before(value = "@annotation(log)")
    public void before(Log log) {
        StopWatch stopWatch = new StopWatch();
        STOP_WATCH_THREAD_LOCAL.set(stopWatch);
        stopWatch.start();
    }

    /**
     * 方法执行后
     */
    @AfterReturning(value = "@annotation(logInject)", returning = "result")
    public void after(JoinPoint joinPoint, Log logInject, Object result) {
        handle(joinPoint, logInject, result, (Exception) null);
    }

    /**
     * 方法执行异常
     */
    @AfterThrowing(value = "@annotation(logInject)", throwing = "e")
    public void throwing(JoinPoint joinPoint, Log logInject, Exception e) {
        handle(joinPoint, logInject, null, e);
    }

    private void handle(final JoinPoint joinPoint, Log logInject, final Object result, final Exception e) {
        try {
            OperateLogEvent event = new OperateLogEvent();
            event.setStatus(OperateStatusEnum.SUCCESS.getCode());
            event.setIp(ServletUtils.getClientIP());
            event.setUrl(StrUtil.sub(ServletUtils.getRequest().getRequestURI(), 0, 255));
            event.setUserId(SecurityHelper.getUserId());
            event.setUsername(SecurityHelper.getUsername());
            event.setNickname(SecurityHelper.getNickname());

            if (ObjectUtil.isNotNull(e)) {
                event.setStatus(OperateStatusEnum.FAIL.getCode());
                event.setErrorMsg(StrUtil.sub(ExceptionUtil.getMessage(e), 0, 3800));
            }

            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            event.setMethod(className + "." + methodName + "()");
            event.setRequestMethod(ServletUtils.getRequest().getMethod());

            handle(joinPoint, logInject, result, event);

            StopWatch stopWatch = STOP_WATCH_THREAD_LOCAL.get();
            stopWatch.stop();
            event.setTakeTime(stopWatch.getTotalTimeMillis());

            SpringUtil.getApplicationContext().publishEvent(event);
        } catch (Exception ex) {
            log.error("操作日志异常", ex);
        } finally {
            STOP_WATCH_THREAD_LOCAL.remove();
        }
    }

    private void handle(JoinPoint joinPoint, Log logInject, Object result, OperateLogEvent event) {
        event.setTitle(logInject.title());
        event.setBusinessType(logInject.businessType().getCode());
        if (logInject.saveRequest()) {
            saveRequest(joinPoint, logInject, event);
        }

        if (logInject.saveResponse()) {
            if (ObjectUtil.isNotNull(result)) {
                event.setResponse(StrUtil.sub(JSONObject.toJSONString(result), 0, 3800));
            }
        }
    }

    private void saveRequest(JoinPoint joinPoint, Log logInject, OperateLogEvent event) {
        Map<String, String> paramsMap = ServletUtil.getParamMap(ServletUtils.getRequest());
        String requestMethod = event.getRequestMethod();
        if (MapUtil.isEmpty(paramsMap) && StrUtil.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) {
            String params = handleRequestParam(joinPoint.getArgs(), logInject.ignoreParams());
            event.setRequest(StrUtil.sub(params, 0, 3800));
        } else {
            MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
            MapUtil.removeAny(paramsMap, logInject.ignoreParams());
            event.setRequest(StrUtil.sub(JSONObject.toJSONString(paramsMap), 0, 3800));
        }
    }

    private String handleRequestParam(Object[] params, String[] ignoreParams) {
        StringBuffer data = new StringBuffer();
        if (ArrayUtil.isEmpty(params)) {
            return data.toString();
        }
        for (Object item : params) {
            if (ObjectUtil.isNotNull(item) && !isIgnore(item)) {
                String itemJson = JSONObject.toJSONString(item);
                Map itemMap = JSONObject.parseObject(itemJson, HashMap.class);
                if (ObjectUtil.isNotNull(itemMap)) {
                    MapUtil.removeAny(itemMap, EXCLUDE_PROPERTIES);
                    MapUtil.removeAny(itemMap, ignoreParams);
                    itemJson = JSONObject.toJSONString(itemMap);
                }
                data.append(itemJson);
            }
        }
        return data.toString();
    }

    private boolean isIgnore(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.values()) {
                return value instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult;
    }
}
